﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Runtime.Caching;
using System.Threading;

namespace IRC.IRepairClouds.DataLayer.Cache
{
    public abstract class CacheManager
    {

        public static TimeZoneStaticCacheManager TimeZones
        {
            get { return Singleton<TimeZoneStaticCacheManager>.Instance; }
        }

        public static UserStaticCacheManager Users
        {
            get { return Singleton<UserStaticCacheManager>.Instance; }
        }


        public static CustomCacheManager Customs
        {
            get { return Singleton<CustomCacheManager>.Instance; }
        }

        public static EntityCacheManager Entities
        {
            get { return Singleton<EntityCacheManager>.Instance; }
        }


        private class CacheItemRemoveAction<TC>
        {
            private CacheManager Owner { get; set; }
            private Func<TC, IEnumerable<CachedKeys>> GetCachedKeys { get; set; }

            public CacheItemRemoveAction(CacheManager owner, Func<TC, IEnumerable<CachedKeys>> getCachedKeys)
            {
                Owner = owner;
                GetCachedKeys = getCachedKeys;
            }

            public void OnRemoved(CacheEntryRemovedArguments arguments)
            {
                TC cachedObject = (TC)arguments.CacheItem.Value;

                var cache = arguments.Source;
                if (GetCachedKeys != null) {
                    foreach (var ck in GetCachedKeys(cachedObject)) {
                        var key = ConcatKeys(Owner.Group, ck.Keys);
                        if (key != arguments.CacheItem.Key)
                            cache.Remove(key);
                    }
                }
            }
        }

        private static object staticSync = new object();
        private static MemoryCache defaultCache;
        private static Dictionary<string, MemoryCache> caches = new Dictionary<string, MemoryCache>();
        private static List<CacheManager> cacheManagers = new List<CacheManager>();
        
        protected object sync = new object();
        protected virtual string CacheManagerName { get { return null; } }
        protected abstract string Group { get; }
        protected virtual CacheItemPriority? CacheScavengingPriority { get { return null; } }
        protected virtual DateTimeOffset? AbsoluteExpiration { get { return null; } }
        protected virtual TimeSpan? SlidingExpiration { get { return null; } }


        private bool _initialLoaded = false;
        private string cacheManagerName;
        private string group;

        private MemoryCache _cache;
        private CacheItemPriority _cacheScavengingPriority;
        private DateTimeOffset absoluteExpiration;
        private TimeSpan slidingExpiration;

        public static List<MemoryCache> GetCaches()
        {
            lock (staticSync) {
                return caches.Values.ToList();
            }
        }

        public static void FlushAll()
        {
            lock (staticSync) {
                List<MemoryCache> allCaches = new List<MemoryCache>();
                if (defaultCache != null)
                    allCaches.Add(defaultCache);
                allCaches.AddRange(caches.Values);
                defaultCache = null;
                caches.Clear();

                foreach (var cm in cacheManagers) {
                    lock (cm.sync) {
                        cm._cache = null;
                    }
                }

                while (allCaches.Count > 0) {
                    var cache = allCaches[0];
                    cache.Dispose();
                    allCaches.RemoveAt(0);
                }
            }
        }

        private static MemoryCache CreateCache(string name)
        {
            lock (staticSync) {
                if (name == null) {
                    if (defaultCache == null)
                        defaultCache = new MemoryCache("Default Cache Manager");
                    return defaultCache;
                }
                else {
                    MemoryCache c;
                    if (!caches.TryGetValue(name, out c)) {
                        c = new MemoryCache(name);
                        caches.Add(name, c);
                    }
                    return c;
                }
            }
        }

        public CacheManager()
        {
            cacheManagerName = CacheManagerName.TrimNull();
            group = Group;
            _cacheScavengingPriority = CacheScavengingPriority ?? CacheItemPriority.Default;
            absoluteExpiration = AbsoluteExpiration ?? ObjectCache.InfiniteAbsoluteExpiration;
            slidingExpiration = SlidingExpiration ?? ObjectCache.NoSlidingExpiration;

            lock (staticSync) {
                cacheManagers.Add(this);
            }
        }

        private MemoryCache Cache
        {
            get
            {
                lock (sync) {
                    if (_cache == null)
                        _cache = CreateCache(cacheManagerName);
                    return _cache;
                }
            }
        }

        protected virtual void InitialLoad()
        {
        }

        private void TriggerInitialLoad()
        {
            if (!_initialLoaded) {
                lock (sync) {
                    if (!_initialLoaded) {
                        InitialLoad();
                        _initialLoaded = true;
                    }
                }
            }
        }

        public static string ConcatKeys(params object[] keys)
        {
            if (keys.Length == 0)
                return string.Empty;
            StringBuilder sb = new StringBuilder();
            ConcatKeys(sb, keys);
            return sb.ToString();
        }

        private static void ConcatKeys(StringBuilder sb, params object[] keys)
        {
            for (int i = 0; i < keys.Length; i++) {
                var key = keys[i];
                object[] akey = key as object[];
                if (akey != null)
                    ConcatKeys(sb, akey);
                else if (key != null) {
                    sb.Append(key.ToString());
                    sb.Append("|");
                }
            }
        }

        private void InsertIntoCache<TC>(TC data, string fullkey, Func<TC, IEnumerable<CachedKeys>> getCachedKeys, CacheItemPriority? cacheScavengingPriority, DateTimeOffset? absoluteExpiration, TimeSpan? slidingExpiration) where TC : class
        {
            if (data == null)
                return;
            if (getCachedKeys != null) {
                var removeAction = new CacheItemRemoveAction<TC>(this, getCachedKeys);
                var keys = getCachedKeys(data).Select(ck => ConcatKeys(Group, ck.Keys)).ToHashSet();
                if (fullkey != null && !keys.Contains(fullkey)) {
                    throw new Exception("Inconsistenci in cache keys [" + this.GetType().Name + "], usedKey =[" + fullkey + "], keys= [" + keys.JSON_BaseSerializeEx() + "].", null);
                }
                foreach (var key in keys)
                    Cache.AddOrGetExisting(key, data, new CacheItemPolicy {
                        Priority = cacheScavengingPriority ?? this._cacheScavengingPriority,
                        AbsoluteExpiration = absoluteExpiration ?? this.absoluteExpiration,
                        SlidingExpiration = slidingExpiration ?? this.slidingExpiration,
                        RemovedCallback = removeAction.OnRemoved,
                    });
            }
            else if (fullkey != null) {
                Cache.AddOrGetExisting(fullkey, data, new CacheItemPolicy {
                    Priority = cacheScavengingPriority ?? this._cacheScavengingPriority,
                    AbsoluteExpiration = absoluteExpiration ?? this.absoluteExpiration,
                    SlidingExpiration = slidingExpiration ?? this.slidingExpiration,
                });
            }
        }

        //protected TC GetBase<TC>(Func<TC> create, object[] keys, CacheItemPriority? cacheScavengingPriority = null, DateTimeOffset? absoluteExpiration = null, TimeSpan? slidingExpiration = null) where TC : class
        //{
        //    return GetBase<TC>(Func<TC> create, Func<TC, IEnumerable<CachedKeys>> getCachedKeys, object[] keys, CacheItemPriority? cacheScavengingPriority = null, DateTimeOffset? absoluteExpiration = null, TimeSpan? slidingExpiration = null) where TC : class
        //}

        protected TC GetBase<TC>(Func<TC> create, Func<TC, IEnumerable<CachedKeys>> getCachedKeys, object[] keys, CacheItemPriority? cacheScavengingPriority = null, DateTimeOffset? absoluteExpiration = null, TimeSpan? slidingExpiration = null) where TC : class
        {
            TriggerInitialLoad();
            var fkey = ConcatKeys(Group, keys);
            TC data;
            lock (sync) {
                data = (TC)Cache.Get(fkey);
            }
            if (data == null) {
                if (create == null) {
                    throw new Exception("Object not found in cache [" + this.GetType().Name + "], keys= [" + keys.JSON_BaseSerializeEx() + "] and no method to create the object assigned.", null);
                    return null;
                }
                TC newData = create();
                if (newData == null) {
                    throw new Exception("Object not found in cache [" + this.GetType().Name + "], keys= [" + keys.JSON_BaseSerializeEx() + "] and the method to create object returned null.", null);
                    return null;
                }
                data = newData;
                lock (sync) {
                    InsertIntoCache<TC>(data, fkey, getCachedKeys, cacheScavengingPriority, absoluteExpiration, slidingExpiration);
                }
            }
            return data;
        }

        protected void InsertInitialLoadBase<TC>(IEnumerable<TC> datas, Func<TC, IEnumerable<CachedKeys>> getCachedKeys, CacheItemPriority? cacheScavengingPriority = null, DateTimeOffset? absoluteExpiration = null, TimeSpan? slidingExpiration = null) where TC : class
        {
            lock (sync) {
                foreach (var data in datas)
                    InsertIntoCache(data, null, getCachedKeys, cacheScavengingPriority, absoluteExpiration, slidingExpiration);
            }
        }
    }


    public abstract class CacheManager<TC> : CacheManager where TC : class
    {
        protected abstract IEnumerable<CachedKeys> GetCachedKeys(TC obj);

        protected TC GetBase(Func<TC> create, object[] keys, CacheItemPriority? cacheScavengingPriority = null, DateTimeOffset? absoluteExpiration = null, TimeSpan? slidingExpiration = null)
        {
            return base.GetBase<TC>(create, this.GetCachedKeys, keys, cacheScavengingPriority, absoluteExpiration, slidingExpiration);
        }

        protected void InsertInitialLoadBase(IEnumerable<TC> datas)
        {
            base.InsertInitialLoadBase(datas, this.GetCachedKeys);
        }
    }
}
